Skip to content

前端 socket.io-client

注意事项

shell
# 关于 io 实例,创建连接和创建命名空间的概念不要混淆
const socket = io("http://localhost:3000/chat");
以上代码的作用有两个功能:
    前提是 需要判断是否已经建立 socket 连接
        未连接的情况下:一是创建 http://localhost:3000 连接通道,二是创建 /chat 命名空间
        已连接的情况下:则省略一,只进入二
    后面还可以写多个 io 实例代码:const game= io('/game')  
        后续的实例不会再创建连接,在已有的连接基础上创建一个子通道,只是创建对应的命名空间
如果不写域名,直接写 const game= io('/game')  且只有这一个实例
    那么,Socket.IO 会自动补全域名和端口,默认连接当前网站的域名 + 当前端口,再创建一个 socket 连接,再创建一个 /game 命名空间
域名/端口是否相同还有不同的说法
    域名 / 端口不同 新建连接
        io('a.com')
        io('b.com')
 两条连接
    域名 / 端口相同 复用连接
        io('a.com')
        io('a.com/game')
 同一条连接,不同命名空间
命名空间 房间
    命名空间:连接上的子通道
    房间:命名空间里的小组

# 总结
1. 不同域名 = 不同连接
2. 相同域名 = 复用连接 + 多命名空间
3. io('/game') 不写域名 = 连接当前网站
4. 连接是物理通道
5. 命名空间是逻辑通道
6. 房间是命名空间下的小组

核心 API

shell
socket.io-client 库的核心是围绕 io 函数、Manager Socket 这三个层次构建的。
io 用于创建连接并获取 Socket 实例;Socket 则用于事件收发,是进行数据通信的核心对象。
Manager 负责底层连接和重连逻辑;

# io
io(url, options) 连接到指定 url 的服务器,options 可配置重连、认证等参数
常用配置选项 (Options)
    auth Object {} 在连接时发送给服务器的认证数据(如 { token: '...' })。
    autoConnect Boolean true 若设为 false,需要手动调用 .connect() 才会发起连接。
    reconnection Boolean true 是否启用自动重连。
    reconnectionAttempts Number Infinity 最大重连尝试次数。
    reconnectionDelay Number 1000 初始重连延迟时间(毫秒),reconnectionDelayMax 可设上限。
    timeout Number 20000 连接超时时间(毫秒)。
    transports Array ['polling', 'websocket'] 指定要使用的传输方式及其顺序。
    query Object {} 作为查询参数附加到连接 URL 上的额外数据。

# 管理器 (Manager)
动态读取或修改 Manager 的配置选项
    .reconnection([value])
    .reconnectionAttempts([value])
    .reconnectionDelay([value])
    .reconnectionDelayMax([value])
    .timeout([value])
核心控制方法
    .open(callback) / .connect(callback):如果初始化时设置了 autoConnect: false,则需要调用此方法手动建立连接。
    .socket(nsp, options):为指定的命名空间(Namespace, nsp)创建一个新的 Socket 实例
监听事件
    error: 发生连接错误时触发。
    reconnect: 成功重新连接后触发。
    reconnect_attempt: 每次尝试重新连接时触发。
    reconnect_error: 重新连接尝试失败时触发。
    reconnect_failed: 重连尝试次数耗尽(reconnectionAttempts)后触发。
    ping: 接收到服务器发来的 ping 包时触发,常用于计算网络延迟。

# Socket 实例的属性和方法
核心属性
    .id:当前 Socket 会话的唯一标识符(String)。在 connect 事件触发后才有效。
    .connected:返回一个布尔值,表示当前是否已连接。
    .disconnected:返回一个布尔值,表示当前是否已断开连接。
核心方法
    .on(eventName, callback):为指定事件(如服务器的自定义消息)添加监听器。
    .once(eventName, callback):添加一个只触发一次的监听器,触发后会自动移除。
    .off([eventName], [listener]):移除事件监听器。若不传参数,则移除所有事件的所有监听器。
    .emit(eventName, [...args], [ack]):向服务器发送一个事件。可以通过回调函数 ack 接收服务器的确认(Acknowledgment)。
    .send([...args], [ack]):.emit('message', ...) 的别名,用于发送消息类型的事件。
    .connect() / .open():如果创建 Socket 时设置了 autoConnect: false,调用此方法手动连接。
    .compress(value):设置一个压缩标志,用于下一个发送的数据包。例如 socket.compress(false).emit('large', data) 可用于禁止压缩大量数据。
监听事件
    connect:成功连接到服务器时触发。
    disconnect:与服务器断开连接时触发。
    connect_error:连接失败时触发。
    reconnect:断线后成功重连时触发。
    reconnecting:正在尝试重连时触发。
    reconnect_attempt:开始一次新的重连尝试时触发。
    reconnect_error:重连尝试失败时触发。
    reconnect_failed:重连次数耗尽,最终失败时触发。

简单场景

1、基础连接与收发消息

js
import io from "socket.io-client";
const socket = io('https://your-http-server.com');
socket.on('connect', () => console.log('已连接'));
socket.on('news', (data) => console.log('收到新闻:', data));
socket.emit('user action', { type: 'click' });

2、认证与手动连接

js
const socket = io({
  auth: { token: 'your-jwt-token' },
  autoConnect: false  // 先不自动连接
});
socket.connect(); // 手动连接

3、命名空间与房间

js
const adminSocket = io('/admin');  // 连接到 /admin 命名空间
const roomSocket = io('https://your-server.com/room/123'); // 连接到特定房间

自动重连

js
let ws;
let lockReconnect = false; // 🔒 加锁:防止重复重连
let reconnectTimer;       // ⏱ 延迟定时器

// 创建连接
function connect() {
  ws = new WebSocket("ws://localhost:3000");

  ws.onopen = () => {
    console.log("连接成功");
  };

  // 断开 → 重连
  ws.onclose = () => {
    reconnect();
  };

  // 报错 → 重连
  ws.onerror = () => {
    reconnect();
  };
}

// 重连函数(核心!包含 延迟 + 锁)
function reconnect() {
  // 🔒 加锁:如果正在重连,直接返回,不重复执行
  if (lockReconnect) return;

  lockReconnect = true; // 上锁

  // ⏱ 加延迟:3 秒后再重连(不会疯狂重连)
  reconnectTimer = setTimeout(() => {
    connect();        // 重新连接
    lockReconnect = false; // 解锁
  }, 3000); // 延迟 3 秒
}

// 启动
connect();

复杂场景

shell
场景类型   关键技术点                 适用场景
确认机制   Acknowledgment           订单、支付、重要操作
心跳检测   定时 ping/pong            弱网环境、移动端
房间管理   Namespaces + Rooms       聊天室、游戏、多租户
离线队列   消息缓存 + 重试            即时通讯、数据上报
文件传输   分块上传 + 进度            大文件上传、音视频
数据节流   限频发送                  鼠标轨迹、传感器数据
二进制传输 Blob/ArrayBuffer         音视频、截图、文件
订阅模式   动态订阅/取消              股票行情、新闻推送
多路复用   Manager + Namespace      多模块应用
错误处理   降级 + 重连策略            生产环境必备

1、带确认机制的消息(Acknowledgment)

适用于需要确保服务器已接收并处理消息的场景,比如支付、订单创建等。

js
// 发送订单创建请求,等待服务器确认
socket.emit('create order', { productId: 123, quantity: 2 }, (response) => {
  if (response.status === 'success') {
    console.log('订单创建成功,订单号:', response.orderId);
  } else {
    console.error('订单创建失败:', response.error);
  }
});

// 服务器端处理(Node.js 示例)
socket.on('create order', (orderData, callback) => {
  // 处理订单逻辑...
  if (success) {
    callback({ status: 'success', orderId: 'ORD-12345' });
  } else {
    callback({ status: 'error', error: '库存不足' });
  }
});

2、心跳检测与自动重连优化

适用于网络不稳定的环境,如移动端、弱网场景。

js
let lastHeartbeat = Date.now();
let heartbeatInterval;

const socket = io('https://your-server.com', {
  reconnection: true,
  reconnectionAttempts: 10,      // 最多重连10次
  reconnectionDelay: 1000,       // 初始延迟1秒
  reconnectionDelayMax: 5000,    // 最大延迟5秒
  timeout: 10000                 // 连接超时10秒
});

// 监听服务器心跳
socket.on('heartbeat', () => {
  lastHeartbeat = Date.now();
});

// 客户端主动发送心跳(可选)
setInterval(() => {
  if (socket.connected) {
    socket.emit('client heartbeat');
  }
}, 30000);

// 检测连接是否假死(长时间未收到心跳)
setInterval(() => {
  if (socket.connected && (Date.now() - lastHeartbeat) > 60000) {
    console.warn('连接可能假死,手动断开重连');
    socket.disconnect();
    socket.connect();
  }
}, 10000);

3、房间(Rooms)与命名空间(Namespaces)管理

适用于多频道聊天、多游戏房间、多租户系统等场景。

js
// 主命名空间 - 全局通知
const mainSocket = io('/');
mainSocket.emit('join room', 'global-notifications');
mainSocket.on('global announcement', (msg) => {
  console.log('全局公告:', msg);
});

// 游戏房间命名空间 - 实时对战
const gameSocket = io('/game', {
  auth: { userId: 'user123' }
});

// 加入指定房间
gameSocket.emit('join game room', 'room-abc123');

// 监听房间内事件
gameSocket.on('player joined', (playerInfo) => {
  console.log(`${playerInfo.name} 加入了房间`);
});

gameSocket.on('game state update', (gameState) => {
  updateGameUI(gameState);
});

// 发送游戏操作
gameSocket.emit('player action', {
  roomId: 'room-abc123',
  action: 'move',
  data: { x: 100, y: 200 }
});

// 离开房间
function leaveRoom(roomId) {
  gameSocket.emit('leave room', roomId);
}

4、消息缓存与离线队列

适用于需要保证消息可靠送达的场景,即使网络断开也要缓存消息。

js
class ReliableSocket {
  constructor(url, options) {
    this.socket = io(url, options);
    this.messageQueue = [];
    this.isConnected = false;
    
    this.socket.on('connect', () => {
      this.isConnected = true;
      this.flushQueue();  // 连接成功后发送缓存的消息
    });
    
    this.socket.on('disconnect', () => {
      this.isConnected = false;
    });
  }
  
  // 可靠发送:如果离线则缓存
  emitReliable(event, data, maxRetries = 3) {
    const message = {
      id: Date.now() + '-' + Math.random(),
      event,
      data,
      retries: 0,
      maxRetries
    };
    
    if (this.isConnected) {
      this.sendWithRetry(message);
    } else {
      console.log('离线状态,缓存消息:', message);
      this.messageQueue.push(message);
    }
  }
  
  sendWithRetry(message) {
    this.socket.emit(message.event, message.data, (ack) => {
      if (ack && ack.success) {
        console.log('消息发送成功:', message.id);
      } else if (message.retries < message.maxRetries) {
        message.retries++;
        console.log(`重试发送消息 ${message.id},第 ${message.retries}`);
        setTimeout(() => this.sendWithRetry(message), 1000 * message.retries);
      } else {
        console.error('消息发送失败:', message);
        this.handleFailedMessage(message);
      }
    });
  }
  
  flushQueue() {
    console.log(`发送 ${this.messageQueue.length} 条缓存消息`);
    while (this.messageQueue.length > 0) {
      const msg = this.messageQueue.shift();
      this.sendWithRetry(msg);
    }
  }
  
  handleFailedMessage(message) {
    // 将失败消息保存到 localStorage 供下次启动时重试
    const failed = JSON.parse(localStorage.getItem('failedMessages') || '[]');
    failed.push(message);
    localStorage.setItem('failedMessages', JSON.stringify(failed));
  }
}

// 使用示例
const reliableSocket = new ReliableSocket('https://your-server.com');
reliableSocket.emitReliable('important data', { userId: 123, content: '重要内容' });

5、进度上传与文件传输

适用于大文件上传、实时进度展示等场景。

js
function uploadFileWithProgress(file) {
  const chunkSize = 64 * 1024; // 64KB 分块
  let offset = 0;
  const totalChunks = Math.ceil(file.size / chunkSize);
  
  function sendNextChunk() {
    const chunk = file.slice(offset, offset + chunkSize);
    const reader = new FileReader();
    
    reader.onload = (e) => {
      const chunkData = {
        fileName: file.name,
        chunkIndex: Math.floor(offset / chunkSize),
        totalChunks: totalChunks,
        data: e.target.result,
        fileId: uploadId
      };
      
      // 发送分块数据
      socket.emit('upload chunk', chunkData, (ack) => {
        if (ack && ack.received) {
          offset += chunkSize;
          const progress = Math.floor((offset / file.size) * 100);
          
          // 触发进度事件
          socket.emit('upload progress', {
            fileId: uploadId,
            progress: progress
          });
          
          if (offset < file.size) {
            sendNextChunk(); // 继续发送下一块
          } else {
            console.log('文件上传完成!');
            socket.emit('upload complete', { fileId: uploadId });
          }
        }
      });
    };
    
    reader.readAsDataURL(chunk);
  }
  
  const uploadId = Date.now() + '-' + file.name;
  sendNextChunk();
}

// 上传进度监听
socket.on('upload progress', (data) => {
  updateProgressBar(data.progress);
  console.log(`上传进度: ${data.progress}%`);
});

6、实时数据流与节流

适用于常见数据(如鼠标移动、股票行情),需要节流避免性能问题。

js
let lastEmit = 0;
const THROTTLE_MS = 100; // 每100毫秒最多发送一次

// 鼠标移动轨迹跟踪
document.addEventListener('mousemove', (e) => {
  const now = Date.now();
  if (now - lastEmit >= THROTTLE_MS) {
    lastEmit = now;
    socket.emit('mouse move', {
      x: e.clientX,
      y: e.clientY,
      timestamp: now
    });
  }
});

// 或者使用 requestAnimationFrame 优化
let animationFrameId;
let lastPosition = { x: 0, y: 0 };

document.addEventListener('mousemove', (e) => {
  lastPosition = { x: e.clientX, y: e.clientY };
  
  if (!animationFrameId) {
    animationFrameId = requestAnimationFrame(() => {
      socket.emit('mouse move', lastPosition);
      animationFrameId = null;
    });
  }
});

7、二进制数据高效传输

适用于音视频流、图片、文件等二进制数据。

js
// 发送音频流
navigator.mediaDevices.getUserMedia({ audio: true })
  .then(stream => {
    const mediaRecorder = new MediaRecorder(stream);
    mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0) {
        // 直接发送 Blob 数据
        socket.emit('audio data', event.data);
      }
    };
    mediaRecorder.start(1000); // 每秒发送一次
  });

// 接收二进制数据并播放
socket.on('audio data', (audioBlob) => {
  const audioUrl = URL.createObjectURL(audioBlob);
  const audio = new Audio(audioUrl);
  audio.play();
});

// 发送 Canvas 截图(二进制)
function sendScreenshot() {
  const canvas = document.getElementById('game-canvas');
  canvas.toBlob((blob) => {
    socket.emit('screenshot', blob);
  }, 'image/jpeg', 0.8);
}

setInterval(sendScreenshot, 5000); // 每5秒发送一次截图

8、动态订阅/取消订阅模式

适用于用户可以灵活开关不同数据流的场景。

js
class DataSubscriptionManager {
  constructor(socket) {
    this.socket = socket;
    this.subscriptions = new Map(); // 存储当前订阅
  }
  
  subscribe(topic, callback) {
    if (!this.subscriptions.has(topic)) {
      this.subscriptions.set(topic, new Set());
      
      // 第一次订阅时,通知服务器
      this.socket.emit('subscribe', topic);
      
      // 为该 topic 添加全局监听器
      this.socket.on(topic, (data) => {
        const callbacks = this.subscriptions.get(topic);
        if (callbacks) {
          callbacks.forEach(cb => cb(data));
        }
      });
    }
    
    this.subscriptions.get(topic).add(callback);
    console.log(`订阅了 ${topic},当前订阅者数: ${this.subscriptions.get(topic).size}`);
    
    // 返回取消订阅函数
    return () => this.unsubscribe(topic, callback);
  }
  
  unsubscribe(topic, callback) {
    const callbacks = this.subscriptions.get(topic);
    if (callbacks) {
      callbacks.delete(callback);
      
      if (callbacks.size === 0) {
        // 最后一个订阅者取消时,通知服务器
        this.socket.emit('unsubscribe', topic);
        this.socket.off(topic);
        this.subscriptions.delete(topic);
        console.log(`取消订阅 ${topic}`);
      }
    }
  }
}

// 使用示例
const subManager = new DataSubscriptionManager(socket);

// 订阅股票行情
const unsubscribeStock = subManager.subscribe('stock:APPL', (data) => {
  console.log(`苹果股价: $${data.price}`);
});

// 订阅天气信息
subManager.subscribe('weather:beijing', (weather) => {
  console.log(`北京天气: ${weather.temp}°C`);
});

// 5分钟后取消股票订阅
setTimeout(() => {
  unsubscribeStock();
  console.log('已取消股票订阅');
}, 5 * 60 * 1000);

9、连接池与多路复用优化

适用于一个页面需要连接多个服务的场景,节省资源。

js
// 不好的做法:创建多个独立连接
// const socket1 = io('https://api1.example.com');
// const socket2 = io('https://api2.example.com');

// 好的做法:使用同一连接的不同命名空间
const manager = io.Manager('https://api.example.com', {
  reconnection: true,
  transports: ['websocket']  // 强制使用 WebSocket
});

// 多个命名空间共享同一个底层连接
const chatSocket = manager.socket('/chat');
const gameSocket = manager.socket('/game');
const notifySocket = manager.socket('/notify');

chatSocket.on('message', (msg) => console.log('聊天消息:', msg));
gameSocket.on('score', (score) => console.log('游戏分数:', score));
notifySocket.on('alert', (alert) => console.log('系统通知:', alert));

// 手动控制连接(节省带宽)
chatSocket.connect();
gameSocket.connect();
notifySocket.connect();

// 暂时不需要聊天功能时,可以断开但保持其他
function disableChat() {
  chatSocket.disconnect();
  // gameSocket 和 notifySocket 仍保持连接
}

10、错误处理与优雅降级

适用于生产环境的完整错误处理方案。

js
class RobustSocket {
  constructor(url, options) {
    this.url = url;
    this.options = {
      reconnection: true,
      reconnectionAttempts: 5,
      timeout: 10000,
      ...options
    };
    
    this.connect();
  }
  
  connect() {
    this.socket = io(this.url, this.options);
    this.setupEventHandlers();
  }
  
  setupEventHandlers() {
    this.socket.on('connect', () => {
      console.log('✅ 连接成功,ID:', this.socket.id);
      this.showToast('连接成功', 'success');
    });
    
    this.socket.on('connect_error', (error) => {
      console.error('❌ 连接失败:', error.message);
      this.showToast('连接服务器失败', 'error');
      
      // 尝试降级方案
      if (error.message.includes('websocket')) {
        console.log('WebSocket 失败,尝试长轮询...');
        this.socket.io.opts.transports = ['polling', 'websocket'];
      }
    });
    
    this.socket.on('disconnect', (reason) => {
      console.warn('⚠️ 断开连接:', reason);
      
      if (reason === 'io server disconnect') {
        // 服务器主动断开,手动重连
        this.socket.connect();
      }
      // 其他原因(如网络问题),自动重连机制会处理
    });
    
    this.socket.on('reconnect_attempt', (attempt) => {
      console.log(`🔄 重连尝试 ${attempt}`);
      this.showToast(`正在重连... (${attempt}/${this.options.reconnectionAttempts})`, 'info');
    });
    
    this.socket.on('reconnect', (attempt) => {
      console.log(`✅ 重连成功 (第 ${attempt} 次尝试)`);
      this.showToast('重新连接成功', 'success');
    });
    
    this.socket.on('reconnect_failed', () => {
      console.error('❌ 重连失败,已达最大尝试次数');
      this.showToast('无法连接到服务器,请刷新页面', 'error');
      this.enterOfflineMode();
    });
    
    this.socket.on('error', (error) => {
      console.error('Socket 错误:', error);
      // 记录错误到监控系统
      this.logErrorToService(error);
    });
  }
  
  showToast(message, type) {
    // 实现 UI 提示
    console.log(`[${type}] ${message}`);
  }
  
  enterOfflineMode() {
    // 进入离线模式,保存用户操作到本地
    console.log('进入离线模式');
  }
  
  logErrorToService(error) {
    // 发送到错误监控服务(如 Sentry)
    if (window.Sentry) {
      Sentry.captureException(error);
    }
  }
  
  emit(event, data, callback) {
    if (this.socket && this.socket.connected) {
      this.socket.emit(event, data, callback);
    } else {
      console.warn(`离线状态,无法发送事件: ${event}`);
      // 缓存到本地
      this.cacheOfflineEvent(event, data);
    }
  }
  
  cacheOfflineEvent(event, data) {
    const cache = JSON.parse(localStorage.getItem('offlineEvents') || '[]');
    cache.push({ event, data, timestamp: Date.now() });
    localStorage.setItem('offlineEvents', JSON.stringify(cache));
  }
}